/*--------------------------------------------- Noble Steed Engine--------------------------------------*
Authors : Daniel Randle, Alex Bourne
Date Created : Mar 6 2013

File:
	nsmesh.cpp

Description:
	This file contains the definition for the NSMesh class functions along with any other helper
	functions that will aid in loading/using meshes
*-----------------------------------------------------------------------------------------------------*/

#include <nstexture.h>
#include <nsmesh.h>
#include <logfile.h>
#include <string>
#include <sstream>


#ifdef DEBUG
#include <debug.h>
#endif

/*---------------------------*
|   class NSMesh BEGINS      |
*---------------------------*/

NSMesh::NSMesh(const char * fileName, const std::string & textureDir, bool hasTransluscentMat):
	hasTransMats(hasTransluscentMat),
	defaultTextureDir("Textures/Default")
{
	LogFile("Creating mesh...");
	if (fileName != "" && textureDir != "")
	{
		if (!loadScene(fileName, textureDir))
			LogFile("Error in NMesh constructor with loading mesh from file");
	}

	#ifdef DEBUG
	d = NULL;
	#endif
}

NSMesh::~NSMesh()
{
	LogFile("Deleting mesh... deleting buffers and materials");
	while (meshMaterials.begin() != meshMaterials.end())
	{
		delete meshMaterials.back();
		meshMaterials.pop_back();
	}
	while (subMeshes.begin() != subMeshes.end())
	{
		glDeleteBuffers(1, &subMeshes.back()->posBuf);
		glDeleteBuffers(1, &subMeshes.back()->texBuf);
		glDeleteBuffers(1, &subMeshes.back()->normBuf);
		glDeleteBuffers(1, &subMeshes.back()->tangBuf);
		glDeleteBuffers(1, &subMeshes.back()->indiceBuf);
		glDeleteBuffers(1, &subMeshes.back()->transformBuf);
		glDeleteVertexArrays(1, &subMeshes.back()->vertexArrObj);
		delete subMeshes.back();
		subMeshes.pop_back();
	}
	LogFile("Mesh Deleted");
}

bool NSMesh::isHasTransMat()
{
	return hasTransMats;
}

bool NSMesh::loadScene(const char * fileName, const std::string & textureDir)
{
	Assimp::Importer importer;
	const aiScene * scene = importer.ReadFile(fileName, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_GenSmoothNormals | aiProcess_CalcTangentSpace | aiProcess_JoinIdenticalVertices |  aiProcess_RemoveRedundantMaterials | aiProcess_OptimizeMeshes );

	const aiNode * parentNode;
	aiMaterial **materials;
	unsigned int numMaterials;
	
	LogFile l("Attempting to load mesh with filename " + std::string(fileName));
	if (scene != NULL)
	{
		parentNode = scene->mRootNode;
		materials = scene->mMaterials;
		numMaterials = scene->mNumMaterials;
	}
	else
	{
		l.write("ERROR: in NSMesh::loadMesh - " + std::string(importer.GetErrorString()));
		return false;
	}

	//NSMatrix4Df transform

	LogFile("Loading parent node..");

	// This is because freaking blender exports a matrix transform that changes the z to y and y to z
	// Instead of using the identity matrix as the parent transform to the root node im using the
	// matrix that turns the blender transform in to the identity matrix
	NSMatrix4Df modMat;
	modMat.value(1,1) = 0.0f;
	modMat.value(2,1) = 1.0f;
	modMat.value(2,2) = 0.0f;
	modMat.value(1,2) = -1.0f;
	loadNodes(parentNode, modMat, scene);


	bool ret = true;
    for (unsigned int i = 0 ; i < numMaterials ; i++) 
    {
        const aiMaterial* currentMaterial = materials[i];
		ret = loadMaterial(currentMaterial, textureDir);
    }

	sortAlphaSubMeshes();
	return ret;
	
}

bool NSMesh::loadNodes(const aiNode * node, const NSMatrix4Df & pTransform, const aiScene * scene)
{
	bool ret = true;
	aiMatrix4x4 trans = node->mTransformation;
	NSMatrix4Df lTransform(NSVec4Df(trans.a1, trans.a2, trans.a3, trans.a4),
						NSVec4Df(trans.b1, trans.b2, trans.b3, trans.b4),
						NSVec4Df(trans.c1, trans.c2, trans.c3, trans.c4),
						NSVec4Df(trans.d1, trans.d2, trans.d3, trans.d4));
	NSMatrix4Df worldTrans = pTransform * lTransform;

	std::stringstream ss;
	ss << "Node name : " << std::string(node->mName.C_Str()) << "  Child nodes : " << node->mNumChildren;
	LogFile s(ss);
	for (int i = 0; i < node->mNumChildren; ++i)
		ret = loadNodes(node->mChildren[i], worldTrans, scene);
	return loadMeshes(node, worldTrans, scene->mMeshes) && ret;
}

bool NSMesh::loadMeshes(const aiNode * node, const NSMatrix4Df & worldTransform, aiMesh ** const meshes)
{
	unsigned int * meshIndexes = node->mMeshes;

	std::stringstream dd;
	dd << "Processing " << node->mNumMeshes << " submeshes for node...";
	LogFile f(dd);
	for (unsigned int i = 0; i < node->mNumMeshes; ++i)
	{
		const aiMesh* currentMesh = meshes[meshIndexes[i]];
		if (currentMesh != NULL)
		{
			SubMesh * subMesh = new SubMesh();
			subMesh->materialIndex = currentMesh->mMaterialIndex;

			// Load all of the vertices in to the vertex list
			LogFile("Loading subMesh vertex data..");
			for (int j = 0; j < currentMesh->mNumVertices; ++j)
			{
				NSVec3Df position, normal, tangent;
				NSVec2Df texCoord;

				position.x = currentMesh->mVertices[j].x;
				position.y = currentMesh->mVertices[j].y;
				position.z = currentMesh->mVertices[j].z;

				if (currentMesh->HasTextureCoords(0))
				{
					texCoord.u = currentMesh->mTextureCoords[0][j].x;
					texCoord.v = currentMesh->mTextureCoords[0][j].y;
				}
				if (currentMesh->HasNormals())
				{
					normal.x = currentMesh->mNormals[j].x;
					normal.y = currentMesh->mNormals[j].y;
					normal.z = currentMesh->mNormals[j].z;
				}
				if (currentMesh->HasTangentsAndBitangents())
				{
					tangent.x = currentMesh->mTangents[j].x;
					tangent.y = currentMesh->mTangents[j].y;
					tangent.z = currentMesh->mTangents[j].z;
				}
				NSVec4Df transPos = worldTransform * NSVec4Df(position, 1.0f);
				NSVec4Df transNorm = worldTransform * NSVec4Df(normal, 0.0f);
				NSVec4Df transTang = worldTransform * NSVec4Df(tangent, 0.0f);

				subMesh->positions.push_back(NSVec3Df(transPos.x, transPos.y, transPos.z));
				subMesh->texCoords.push_back(texCoord);
				subMesh->normals.push_back(NSVec3Df(transNorm.x, transNorm.y, transNorm.z));
				subMesh->tangents.push_back(NSVec3Df(transTang.x, transTang.y, transTang.z));
			}

			// Load all the indices in to the indice list
			LogFile("Loading subMesh indices..");

			for (int j = 0; j < currentMesh->mNumFaces; ++j)
			{
				const aiFace * currentFace = &currentMesh->mFaces[j];
				for (int k = 0; k < currentFace->mNumIndices; ++k)
					subMesh->indices.push_back(currentFace->mIndices[k]);
			}
			subMesh->load();
			subMeshes.push_back(subMesh);
		}
		else
		{
			LogFile("Current mesh is NULL pointer");
			return false;
		}
	}
	LogFile("Finished loading meshes in this node");
	return true;
}

bool NSMesh::loadMaterial(const aiMaterial * currentMaterial, const std::string & textureDir)
{
	LogFile l("Loading subMesh material...");
	NSMaterial * materialToAdd = new NSMaterial();
	bool ret = true;
	if (currentMaterial->GetTextureCount(aiTextureType_NONE ) > 0)
		l.write("Found dummy value texture");
    if (currentMaterial->GetTextureCount(aiTextureType_DIFFUSE) > 0)
	{
		aiString Path;
		l.write("Found diffuse map" + std::string(Path.C_Str()) );
		if (currentMaterial->GetTexture(aiTextureType_DIFFUSE, 0, &Path, NULL, NULL, NULL, NULL, NULL) == AI_SUCCESS)
		{
			std::string filename = Path.C_Str();
			filename = filename.substr(filename.find_last_of("/\\")+1);
			ret = materialToAdd->loadTextureFromFile(filename,textureDir,NSTexture::Diffuse);
		}
		else
		{
			l.write("Error with assimp loading Diffuse texture");
			ret = false;
		}
	}
	if (currentMaterial->GetTextureCount(aiTextureType_SPECULAR) > 0)
	{
		aiString Path;
		l.write("Found specular map");
		if (currentMaterial->GetTexture(aiTextureType_SPECULAR, 0, &Path, NULL, NULL, NULL, NULL, NULL) == AI_SUCCESS)
		{
			std::string filename = Path.C_Str();
			filename = filename.substr(filename.find_last_of("/\\")+1);
			ret = materialToAdd->loadTextureFromFile(filename,textureDir,NSTexture::Specular);
		}
		else
		{
			l.write("Error with assimp loading Specular texture");
			ret = false;
		}
	}
	if (currentMaterial->GetTextureCount(aiTextureType_AMBIENT) > 0)
	{
		aiString Path;
		l.write("Found ambient map");
		if (currentMaterial->GetTexture(aiTextureType_AMBIENT, 0, &Path, NULL, NULL, NULL, NULL, NULL) == AI_SUCCESS)
		{
			std::string filename = Path.C_Str();
			filename = filename.substr(filename.find_last_of("/\\")+1);
			ret = materialToAdd->loadTextureFromFile(filename,textureDir,NSTexture::Ambient);
		}
		else
		{
			l.write("Error with assimp loading Ambient texture");
			ret = false;
		}
	}
	if (currentMaterial->GetTextureCount(aiTextureType_EMISSIVE) > 0)
	{
		aiString Path;
		l.write("Found emissive map");
		if (currentMaterial->GetTexture(aiTextureType_EMISSIVE, 0, &Path, NULL, NULL, NULL, NULL, NULL) == AI_SUCCESS)
		{
			std::string filename = Path.C_Str();
			filename = filename.substr(filename.find_last_of("/\\")+1);
			ret = materialToAdd->loadTextureFromFile(filename,textureDir,NSTexture::Emissive);
		}
		else
		{
			l.write("Error with assimp loading Emissive texture");
			ret = false;
		}
	}
	if (currentMaterial->GetTextureCount(aiTextureType_HEIGHT) > 0)
	{
		aiString Path;
		l.write("Found height map");
		if (currentMaterial->GetTexture(aiTextureType_HEIGHT, 0, &Path, NULL, NULL, NULL, NULL, NULL) == AI_SUCCESS)
		{
			std::string filename = Path.C_Str();
			filename = filename.substr(filename.find_last_of("/\\")+1);
			ret = materialToAdd->loadTextureFromFile(filename,textureDir,NSTexture::Height);
		}
		else
		{
			l.write("Error with assimp loading Height texture");
			ret = false;
		}
	}
	if (currentMaterial->GetTextureCount(aiTextureType_NORMALS) > 0)
	{
		aiString Path;
		l.write("Found normal map");
		if (currentMaterial->GetTexture(aiTextureType_NORMALS, 0, &Path, NULL, NULL, NULL, NULL, NULL) == AI_SUCCESS)
		{
			std::string filename = Path.C_Str();
			filename = filename.substr(filename.find_last_of("/\\")+1);
			ret = materialToAdd->loadTextureFromFile(filename,textureDir,NSTexture::Normal);
		}
		else
		{
			l.write("Error with assimp loading Normal texture");
			ret = false;
		}
	}
	if (currentMaterial->GetTextureCount(aiTextureType_SHININESS) > 0)
	{
		aiString Path;
		l.write("Found shininess map");
		if (currentMaterial->GetTexture(aiTextureType_SHININESS, 0, &Path, NULL, NULL, NULL, NULL, NULL) == AI_SUCCESS)
		{
			std::string filename = Path.C_Str();
			filename = filename.substr(filename.find_last_of("/\\")+1);
			ret = materialToAdd->loadTextureFromFile(filename,textureDir,NSTexture::Shininess);
		}
		else
		{
			l.write("Error with assimp loading Shininess texture");
			ret = false;
		}
	}
	if (currentMaterial->GetTextureCount(aiTextureType_OPACITY) > 0)
	{
		aiString Path;
		l.write("Found opacity map");
		if (currentMaterial->GetTexture(aiTextureType_OPACITY, 0, &Path, NULL, NULL, NULL, NULL, NULL) == AI_SUCCESS)
		{
			std::string filename = Path.C_Str();
			filename = filename.substr(filename.find_last_of("/\\")+1);
			ret = materialToAdd->loadTextureFromFile(filename,textureDir,NSTexture::Opacity,false);
		}
		else
		{
			l.write("Error with assimp loading Opacity texture");
			ret = false;
		}
	}
	else
	{
		std::string filename = "def.png";
		ret = materialToAdd->loadTextureFromFile(filename,defaultTextureDir,NSTexture::Opacity);
	}
	if (currentMaterial->GetTextureCount(aiTextureType_DISPLACEMENT) > 0)
	{
		aiString Path;
		l.write("Found displacement map");
		if (currentMaterial->GetTexture(aiTextureType_DISPLACEMENT, 0, &Path, NULL, NULL, NULL, NULL, NULL) == AI_SUCCESS)
		{
			std::string filename = Path.C_Str();
			filename = filename.substr(filename.find_last_of("/\\")+1);
			ret = materialToAdd->loadTextureFromFile(filename,textureDir,NSTexture::Displacement);
		}
		else
		{
			l.write("Error with assimp loading Displacement texture");
			ret = false;
		}
	}
	if (currentMaterial->GetTextureCount(aiTextureType_LIGHTMAP) > 0)
	{
		aiString Path;
		l.write("Found light map");
		if (currentMaterial->GetTexture(aiTextureType_DIFFUSE, 0, &Path, NULL, NULL, NULL, NULL, NULL) == AI_SUCCESS)
		{
			std::string filename = Path.C_Str();
			filename = filename.substr(filename.find_last_of("/\\")+1);
			ret = materialToAdd->loadTextureFromFile(filename,textureDir,NSTexture::Lightmap);
		}
		else
		{
			l.write("Error with assimp loading Lightmap texture");
			ret = false;
		}
	}
	if (currentMaterial->GetTextureCount(aiTextureType_REFLECTION) > 0)
	{
		aiString Path;
		l.write("Found reflection map");
		if (currentMaterial->GetTexture(aiTextureType_REFLECTION, 0, &Path, NULL, NULL, NULL, NULL, NULL) == AI_SUCCESS)
		{
			std::string filename = Path.C_Str();
			filename = filename.substr(filename.find_last_of("/\\")+1);
			ret = materialToAdd->loadTextureFromFile(filename,textureDir,NSTexture::Reflection);
		}
		else
		{
			l.write("Error with assimp loading Reflection texture");
			ret = false;
		}
	}
	if (currentMaterial->GetTextureCount(aiTextureType_UNKNOWN) > 0)
		l.write("Found unkown texture map - engine cannot load");

	if (ret)
	{
		meshMaterials.push_back(materialToAdd);
		l.write("Mesh successfully loaded material");
	}
	return ret;
}

void NSMesh::draw(std::vector<NSMatrix4Df> * translationMatrixes, bool rebuildBuf)
{
	if (translationMatrixes->empty())
		return;

	std::vector<SubMesh*>::iterator iter = subMeshes.begin();
	while (iter != subMeshes.end())
	{
		enableMaterialTextures((*iter)->materialIndex);
		(*iter)->draw(translationMatrixes,GL_TRIANGLES,rebuildBuf);
		disableMaterialTextures((*iter)->materialIndex);
		++iter;
	}
}

void NSMesh::drawSingle(const NSMatrix4Df & transform, bool rebuildBuf)
{
	std::vector<SubMesh*>::iterator iter = subMeshes.begin();
	while (iter != subMeshes.end())
	{
		enableMaterialTextures((*iter)->materialIndex);
		(*iter)->drawSingle(transform,GL_TRIANGLES,rebuildBuf);
		disableMaterialTextures((*iter)->materialIndex);
		++iter;
	}
}

void NSMesh::enableMaterialTextures(int matIndex)
{
	if (matIndex >= meshMaterials.size())
	{
		LogFile("Trying to access mesh material with invalid index");
		return;
	}
	NSMaterial * mat = meshMaterials[matIndex];
	mat->useAllValidTextures();
}

void NSMesh::disableMaterialTextures(int matIndex)
{
	if (matIndex >= meshMaterials.size())
	{
		LogFile("Trying to access mesh material with invalid index");
		return;
	}
	NSMaterial * mat = meshMaterials[matIndex];
	mat->disableAllValidTextures();
}

void NSMesh::setRenderMode(RenderMode mode)
{
	renderMode = mode;
}

NSMesh::RenderMode NSMesh::getRenderMode() const
{
	return renderMode;
}


#ifdef DEBUG
void NSMesh::setDebug(Debug *_d)
{
	d = _d;
}
#endif


void NSMesh::sortAlphaSubMeshes()
{
	if (!hasTransMats)
		return;

	std::vector<SubMesh*> alphaVec;
	std::vector<SubMesh*>::iterator iter = subMeshes.begin();
	while (iter != subMeshes.end())
	{
		NSMaterial * mat = meshMaterials[(*iter)->materialIndex];
		if (mat->isAlphaBlend())
		{
			alphaVec.push_back(*iter);
			iter = subMeshes.erase(iter);
			continue;
		}
		++iter;
	}
	subMeshes.insert(subMeshes.end(),alphaVec.begin(),alphaVec.end());
}

void NSMesh::SubMesh::load()
{
	glGenVertexArrays(1, &vertexArrObj);
	glGenBuffers(1, &posBuf);
	glGenBuffers(1, &texBuf);
	glGenBuffers(1, &normBuf);
	glGenBuffers(1, &tangBuf);
	glGenBuffers(1, &indiceBuf);
	glGenBuffers(1, &transformBuf);

	glBindVertexArray(vertexArrObj);

	glBindBuffer(GL_ARRAY_BUFFER, posBuf);
	glBufferData(GL_ARRAY_BUFFER, sizeof(NSVec3Df) * positions.size(), &positions[0], GL_STATIC_DRAW);
	glEnableVertexAttribArray(POS_ATT);
	glVertexAttribPointer(POS_ATT, 3, GL_FLOAT, GL_FALSE, sizeof(NSVec3Df), 0);

	glBindBuffer(GL_ARRAY_BUFFER, texBuf);
	glBufferData(GL_ARRAY_BUFFER, sizeof(NSVec2Df) * texCoords.size(), &texCoords[0], GL_STATIC_DRAW);
	glEnableVertexAttribArray(TEX_ATT);
	glVertexAttribPointer(TEX_ATT, 2, GL_FLOAT, GL_FALSE, sizeof(NSVec2Df), 0);

	glBindBuffer(GL_ARRAY_BUFFER, normBuf);
	glBufferData(GL_ARRAY_BUFFER, sizeof(NSVec3Df) * normals.size(), &normals[0], GL_STATIC_DRAW);
	glEnableVertexAttribArray(NORM_ATT);
	glVertexAttribPointer(NORM_ATT, 3, GL_FLOAT, GL_FALSE, sizeof(NSVec3Df), 0);

	glBindBuffer(GL_ARRAY_BUFFER, tangBuf);
	glBufferData(GL_ARRAY_BUFFER, sizeof(NSVec3Df) * tangents.size(), &tangents[0], GL_STATIC_DRAW);
	glEnableVertexAttribArray(TANG_ATT);
	glVertexAttribPointer(TANG_ATT, 3, GL_FLOAT, GL_FALSE, sizeof(NSVec3Df), 0);

    glBindBuffer(GL_ARRAY_BUFFER, transformBuf);

	glEnableVertexAttribArray(TRANS1_ATT);
	glVertexAttribPointer(TRANS1_ATT, 4, GL_FLOAT, GL_FALSE, sizeof(NSMatrix4Df), 0);
	glVertexAttribDivisor(TRANS1_ATT, 1);

	glEnableVertexAttribArray(TRANS2_ATT);
	glVertexAttribPointer(TRANS2_ATT, 4, GL_FLOAT, GL_FALSE, sizeof(NSMatrix4Df), (const GLvoid*)(sizeof(NSVec4Df)));
	glVertexAttribDivisor(TRANS2_ATT, 1);

	glEnableVertexAttribArray(TRANS3_ATT);
	glVertexAttribPointer(TRANS3_ATT, 4, GL_FLOAT, GL_FALSE, sizeof(NSMatrix4Df), (const GLvoid*)(sizeof(NSVec4Df) * 2));
	glVertexAttribDivisor(TRANS3_ATT, 1);

	glEnableVertexAttribArray(TRANS4_ATT);
	glVertexAttribPointer(TRANS4_ATT, 4, GL_FLOAT, GL_FALSE, sizeof(NSMatrix4Df), (const GLvoid*)(sizeof(NSVec4Df) * 3));
	glVertexAttribDivisor(TRANS4_ATT, 1);


	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indiceBuf);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(unsigned int) * indices.size(), &indices[0], GL_STATIC_DRAW);

	glBindVertexArray(0);
}

void NSMesh::SubMesh::draw(std::vector<NSMatrix4Df> * translationMatrixes, GLenum primType, bool rebuildBuf)
{

	glBindBuffer(GL_ARRAY_BUFFER, transformBuf);
	if (rebuildBuf)
		glBufferData(GL_ARRAY_BUFFER, sizeof(NSMatrix4Df) * translationMatrixes->size(), &((*translationMatrixes)[0]), GL_DYNAMIC_DRAW);

	glBindVertexArray(vertexArrObj);

	glDrawElementsInstanced(primType, indices.size(), GL_UNSIGNED_INT, 0, translationMatrixes->size());
	glBindVertexArray(0);
}

void NSMesh::SubMesh::drawSingle(const NSMatrix4Df & transform, GLenum primType, bool rebuildBuf)
{
	NSMatrix4Df trans = transform;

	glBindBuffer(GL_ARRAY_BUFFER, transformBuf);

	if (rebuildBuf)
		glBufferData(GL_ARRAY_BUFFER, sizeof(NSMatrix4Df), trans.getDataPtr(), GL_DYNAMIC_DRAW);

	glBindVertexArray(vertexArrObj);
	glDrawElements(primType, indices.size(), GL_UNSIGNED_INT, 0);
	glBindVertexArray(0);
}

NSMesh::SubMeshIter NSMesh::subMeshBegin()
{
	return subMeshes.begin();
}

NSMesh::SubMeshIter NSMesh::subMeshEnd()
{
	return subMeshes.end();
}

NSMesh::ConstSubMeshIter NSMesh::subMeshBegin() const
{
	return subMeshes.begin();
}

NSMesh::ConstSubMeshIter NSMesh::subMeshEnd() const
{
	return subMeshes.end();
}


/*---------------------------*
|   class NSMesh ENDS       |
*---------------------------*/